Skip to content

Add support for configuring domains starting with a number via environment variables (by using _ prefix)#3414

Open
ppenguin wants to merge 2 commits intobunkerity:devfrom
ppenguin:add-numbered-envvars
Open

Add support for configuring domains starting with a number via environment variables (by using _ prefix)#3414
ppenguin wants to merge 2 commits intobunkerity:devfrom
ppenguin:add-numbered-envvars

Conversation

@ppenguin
Copy link
Copy Markdown

@ppenguin ppenguin commented Apr 6, 2026

Closes #1857

Allows prefixing environment variables with _ with the main purpose to allow (v)host/domain names starting with a number (e.g. 1mthe1.org -> _1mthe1.org_USE_LETS_ENCRYPT_WILDCARD="yes").

The _ prefix has been removed from the exclude prefixes list, but the _ prefix is stripped from the variable name(s) as early as possible, so all further logic handles the server names etc. unchanged, i.e. as 1mthe1.org in this example.

Also adds NOMAD_ to the exluded prefixes list to make startup in nomad deployments less noisy.

To support domains starting with a number configured through env vars,
we need to prefix them with _ and allow such variables to be handled for
config generation.

fix env var strip
@ppenguin ppenguin changed the base branch from master to dev April 6, 2026 14:50
Copy link
Copy Markdown
Member

@TheophileDiot TheophileDiot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@TheophileDiot TheophileDiot added the enhancement New feature or request label Apr 7, 2026
@bunkerity bunkerity deleted a comment from coderabbitai bot Apr 10, 2026
@bunkerity bunkerity deleted a comment from coderabbitai bot Apr 10, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

src/common

Configuration variable normalization for numeric-prefixed domain names

  • Configurator.py: Added environment variable name preprocessing to strip a single leading underscore (_) from all variable keys before processing. This enables users to configure domains or vhosts starting with numerals via environment variables (Bash disallows variable names beginning with digits). Example: _1nteresting.io_USE_LETS_ENCRYPT_WILDCARD="yes" normalises to 1nteresting.io internally. Collision detection logs warnings when multiple original keys normalise to the same stripped key; last-encountered value is retained.

  • Excluded prefixes: Removed _ from the excluded-prefixes list and added NOMAD_ to reduce startup noise in Nomad deployments.

  • Processing order: Normalisation occurs immediately after variable loading, before MULTISITE evaluation and server mapping, affecting all downstream lookups.

User-visible behaviour change: Users can now configure numeric-prefixed domains via environment variables by prefixing the variable name with underscore (e.g., _1domain.com_SETTING="value"). The underscore is transparently stripped during initialisation.

Configuration/schema changes: No schema changes; this is a transparent input normalisation feature within the existing Configurator class.

Documentation/tests: No documentation or tests were updated in this commit.

Security impact: None identified. The normalisation is a purely mechanical string transformation applied early in processing, before validation.

Walkthrough

The change implements underscore-prefixed environment variable stripping in Configurator to enable configuration of domain names starting with numerals. Variables with leading underscores are normalised before downstream processing, with collision detection logging warnings when multiple keys map to the same stripped key.

Changes

Cohort / File(s) Summary
Variable preprocessing and exclusion updates
src/common/gen/Configurator.py
Removed _ from __excluded_prefixes, added NOMAD_ to exclusions. Implemented preprocessing loop in __init__ that strips single leading underscore from all variable keys via removeprefix("_"), drops empty-result entries, and logs warnings on key collisions before MULTISITE evaluation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
Allow underscore-prefixed environment variables for domains starting with numbers [#1857]
Strip leading underscore during variable processing [#1857]
Preserve correct domain name without underscore in downstream config [#1857]

Out-of-scope changes

Code Change Explanation
Addition of NOMAD_ to __excluded_prefixes tuple (src/common/gen/Configurator.py) Not mentioned in issue #1857; appears to be an unrelated exclusion rule addition that may warrant separate justification or issue linkage.

Poem

An underscore steps forth with determined grace,
Smuggling numbers into domains' rightful place,
Leading underscores fade away without a trace,
Now _1nteresting.io finds its home, its space. ✨

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly describes the main change—adding support for underscore-prefixed environment variables to configure domains starting with numbers—which directly aligns with the changeset and issue #1857.
Description check ✅ Passed The description directly relates to the changeset, explaining the underscore prefix feature for numbered domains, the stripping logic, and the addition of NOMAD_ to excluded prefixes, with reference to issue #1857.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@TheophileDiot
Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/common/gen/Configurator.py`:
- Around line 114-127: The collision warning in Configurator (where
self.__variables is processed and assigned to stripped) can report misleading
names because it only shows the post-strip key; update the logic to track the
original source key for each entry (e.g., store new_key -> (orig_key, value)
while building stripped) so that when you detect a collision you can call
self.__logger.warning with both the previously stored orig_key and the current
k, and then store/overwrite the value with the current one before finally
collapsing the mapping back to new_key -> value for self.__variables; adjust the
warning text accordingly to show both original keys and that the last value is
kept.
- Around line 118-127: The collision handling in the loop over self.__variables
uses "last wins" which is iteration-order dependent; change it to a
deterministic rule that always prefers the explicit non-underscore key over the
underscore-prefixed form: when computing new_key = k.removeprefix("_") and
encountering new_key in stripped, check whether the existing entry in stripped
came from a non-prefixed original (i.e., the existing source key did not start
with "_") and if so keep the existing value and do not overwrite; otherwise
(existing was from a prefixed key and current k is non-prefixed) overwrite;
update the warning via self.__logger.warning accordingly and ensure the logic
references __variables, new_key, stripped, and removeprefix so behavior is
deterministic regardless of iteration order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e3cadd0b-0947-4bb4-8ea8-3a25f14f6ba1

📥 Commits

Reviewing files that changed from the base of the PR and between 01755bd and 6b6b51d.

📒 Files selected for processing (1)
  • src/common/gen/Configurator.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

⚙️ CodeRabbit configuration file

**/*.py: Follow BunkerWeb's Python standards and security posture:

  • Use snake_case for functions and variables, PascalCase for classes, and provide concise, accurate docstrings for public classes, functions, and methods.
  • Respect Black formatting with a 160-character line limit and the existing pre-commit conventions. Do not insist on adding type annotations to previously untyped code, but accept them when added consistently.
  • Catch specific exceptions; never use bare except:. Catching Exception is acceptable only at explicit process boundaries (for example scheduler loops, outer job runners, worker entrypoints, or graceful-shutdown boundaries) when the code logs enough context and either re-raises, returns an error status, or terminates safely.
  • Never use os.system, subprocess.*(..., shell=True), eval, or exec. Pass subprocess arguments as a list and prefer explicit binary paths for privileged operations.
  • Do not use unsafe deserialisers (pickle, marshal, shelve, jsonpickle, dill) for untrusted data. Use yaml.safe_load() rather than unsafe YAML loading.
  • Open files with an explicit encoding (normally utf-8) and use with statements for files, sockets, database sessions, and temporary resources.
  • Use secrets for token generation and hmac.compare_digest for token, HMAC, or signature comparisons.
  • For HTTP clients (requests, httpx): always set an explicit timeout, validate destination URLs to block RFC1918/loopback/link-local ranges (SSRF), disable automatic redirects to internal hosts, and be careful with proxy settings.
  • For filesystem operations: resolve paths with Path.resolve() and verify they remain under the intended base directory before reading or writing (path traversal).
  • Use defusedxml rather than stdlib XML parsers for untrusted XML.
  • For SQLAlchemy, use bound parameters and safe query construction. Never call text() with f-strings or .format(), and flag .execute() calls built...

Files:

  • src/common/gen/Configurator.py
src/common/gen/**/*.py

⚙️ CodeRabbit configuration file

src/common/gen/**/*.py: src/common/gen/ drives settings validation and config generation:

  • Preserve determinism: the same validated settings should render the same output.
  • Do not bypass the configurator or validation layer when introducing new settings.
  • Any template change that depends on a new setting should come with the corresponding schema or plugin metadata update.
  • Be careful with escaping and quoting so user-supplied settings cannot break out of generated directives or file formats.

Files:

  • src/common/gen/Configurator.py
🔇 Additional comments (1)
src/common/gen/Configurator.py (1)

52-52: LGTM on excluded-prefixes update.

Removing _ from __excluded_prefixes allows underscore-prefixed variables through for subsequent stripping, and adding NOMAD_ reduces log noise in Nomad deployments. Both changes align with the PR objectives.

Comment on lines +114 to +127
# Allow a leading underscore prefix on env var names so that domain names
# starting with a digit can be configured (bash forbids var names starting
# with a digit, e.g. 1nteresting.io). Users write _1nteresting.io_USE_...
# and we strip the single leading underscore here before any other processing.
stripped: Dict[str, str] = {}
for k, v in self.__variables.items():
new_key = k.removeprefix("_")
if not new_key:
# Skip bare "_" or similar empty-after-strip keys
continue
if new_key in stripped:
self.__logger.warning(f"Variable collision after stripping leading underscore: both {k!r} and {new_key!r} exist, keeping last value")
stripped[new_key] = v
self.__variables = stripped
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Collision warning message is misleading when the non-prefixed key is processed second.

When iteration order means _FOO is processed before FOO, the stripped key FOO is already in stripped. The current log reads:

"both 'FOO' and 'FOO' exist, keeping last value"

This doesn't identify the original underscore-prefixed key that caused the collision. Consider tracking which original key produced each entry in stripped so the warning can report both original names accurately.

🔧 Suggested fix
-        stripped: Dict[str, str] = {}
+        stripped: Dict[str, str] = {}
+        stripped_origins: Dict[str, str] = {}  # maps new_key -> original key
         for k, v in self.__variables.items():
             new_key = k.removeprefix("_")
             if not new_key:
                 # Skip bare "_" or similar empty-after-strip keys
                 continue
             if new_key in stripped:
-                self.__logger.warning(f"Variable collision after stripping leading underscore: both {k!r} and {new_key!r} exist, keeping last value")
+                self.__logger.warning(
+                    f"Variable collision after stripping leading underscore: "
+                    f"{stripped_origins[new_key]!r} and {k!r} both map to {new_key!r}, keeping value from {k!r}"
+                )
             stripped[new_key] = v
+            stripped_origins[new_key] = k
         self.__variables = stripped
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common/gen/Configurator.py` around lines 114 - 127, The collision warning
in Configurator (where self.__variables is processed and assigned to stripped)
can report misleading names because it only shows the post-strip key; update the
logic to track the original source key for each entry (e.g., store new_key ->
(orig_key, value) while building stripped) so that when you detect a collision
you can call self.__logger.warning with both the previously stored orig_key and
the current k, and then store/overwrite the value with the current one before
finally collapsing the mapping back to new_key -> value for self.__variables;
adjust the warning text accordingly to show both original keys and that the last
value is kept.

Comment on lines +118 to +127
stripped: Dict[str, str] = {}
for k, v in self.__variables.items():
new_key = k.removeprefix("_")
if not new_key:
# Skip bare "_" or similar empty-after-strip keys
continue
if new_key in stripped:
self.__logger.warning(f"Variable collision after stripping leading underscore: both {k!r} and {new_key!r} exist, keeping last value")
stripped[new_key] = v
self.__variables = stripped
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

"Last wins" collision resolution is iteration-order dependent.

As per coding guidelines for src/common/gen/, determinism should be preserved—the same input should produce the same output. When both FOO and _FOO exist, the surviving value depends on dict iteration order, which may vary if variables originate from environment (historically unordered on older Pythons or external orchestrators) rather than file parsing.

Consider a deterministic tie-breaker, such as always preferring the explicit (non-underscore) key over the underscore-prefixed form.

♻️ Possible deterministic approach
         for k, v in self.__variables.items():
             new_key = k.removeprefix("_")
             if not new_key:
                 continue
-            if new_key in stripped:
-                self.__logger.warning(f"Variable collision after stripping leading underscore: both {k!r} and {new_key!r} exist, keeping last value")
-            stripped[new_key] = v
+            if new_key in stripped:
+                # Prefer explicit (non-prefixed) key; only overwrite if current key is non-prefixed
+                if k == new_key:
+                    self.__logger.warning(
+                        f"Variable collision: {new_key!r} overrides previously stripped key"
+                    )
+                    stripped[new_key] = v
+                else:
+                    self.__logger.warning(
+                        f"Variable collision: keeping explicit {new_key!r}, ignoring {k!r}"
+                    )
+            else:
+                stripped[new_key] = v
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stripped: Dict[str, str] = {}
for k, v in self.__variables.items():
new_key = k.removeprefix("_")
if not new_key:
# Skip bare "_" or similar empty-after-strip keys
continue
if new_key in stripped:
self.__logger.warning(f"Variable collision after stripping leading underscore: both {k!r} and {new_key!r} exist, keeping last value")
stripped[new_key] = v
self.__variables = stripped
stripped: Dict[str, str] = {}
for k, v in self.__variables.items():
new_key = k.removeprefix("_")
if not new_key:
# Skip bare "_" or similar empty-after-strip keys
continue
if new_key in stripped:
# Prefer explicit (non-prefixed) key; only overwrite if current key is non-prefixed
if k == new_key:
self.__logger.warning(
f"Variable collision: {new_key!r} overrides previously stripped key"
)
stripped[new_key] = v
else:
self.__logger.warning(
f"Variable collision: keeping explicit {new_key!r}, ignoring {k!r}"
)
else:
stripped[new_key] = v
self.__variables = stripped
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common/gen/Configurator.py` around lines 118 - 127, The collision
handling in the loop over self.__variables uses "last wins" which is
iteration-order dependent; change it to a deterministic rule that always prefers
the explicit non-underscore key over the underscore-prefixed form: when
computing new_key = k.removeprefix("_") and encountering new_key in stripped,
check whether the existing entry in stripped came from a non-prefixed original
(i.e., the existing source key did not start with "_") and if so keep the
existing value and do not overwrite; otherwise (existing was from a prefixed key
and current k is non-prefixed) overwrite; update the warning via
self.__logger.warning accordingly and ensure the logic references __variables,
new_key, stripped, and removeprefix so behavior is deterministic regardless of
iteration order.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] handle domain names starting with a number via env var prefix

3 participants